﻿// StarSystemDescriptor.cs
//
// Copyright (c) 2007 Mike Strobel
//
// This source code is subject to the terms of the Microsoft Reciprocal License (Ms-RL).
// For details, see <http://www.opensource.org/licenses/ms-rl.html>.
//
// All other rights reserved.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows.Markup;
using System.Xml;
using System.Xml.Schema;

using Supremacy.Utility;

namespace Supremacy.Universe
{
    [Serializable]
    public class HomeSystemsDatabase : Dictionary<string, StarSystemDescriptor>
    {
        private const string XmlFilePath = "Resources/Data/HomeSystems.xml";

        private static void ValidateXml(object sender, ValidationEventArgs e)
        {
            XmlHelper.ValidateXml(XmlFilePath, e);
        }

        public static HomeSystemsDatabase Load()
        {
            XmlSchemaSet schemas = new XmlSchemaSet();
            HomeSystemsDatabase db = new HomeSystemsDatabase();
            XmlDocument xmlDoc = new XmlDocument();

            XmlElement xmlRoot;

            schemas.Add("Supremacy:Supremacy.xsd",
                        "Resources/Data/Supremacy.xsd");
            schemas.Add("Supremacy:TechObjectDatabase.xsd",
                        "Resources/Data/TechObjectDatabase.xsd");

            xmlDoc.Load(XmlFilePath);
            xmlDoc.Schemas.Add(schemas);
            xmlDoc.Validate(ValidateXml);

            xmlRoot = xmlDoc.DocumentElement;

            foreach (XmlElement homeSystemElement in xmlRoot.GetElementsByTagName("HomeSystem"))
            {
                string civId = homeSystemElement.GetAttribute("Civilization").Trim().ToUpperInvariant();
                db[civId] = new StarSystemDescriptor(homeSystemElement["StarSystem"]);
            }

            return db;
        }

        public void Save()
        {
            string path = Path.Combine(
                Environment.CurrentDirectory,
                XmlFilePath);
            Save(path);
        }

        public void Save(string fileName)
        {
            if (fileName == null)
                throw new ArgumentNullException("fileName");
            using (StreamWriter writer = new StreamWriter(fileName))
            {
                Save(writer);
            }
        }

        public void Save(TextWriter writer)
        {
            using (XmlTextWriter xmlWriter = new XmlTextWriter(writer))
            {
                XmlDocument xmlDoc = new XmlDocument();
                XmlElement rootElement = xmlDoc.CreateElement("HomeSystems");

                xmlWriter.Formatting = Formatting.Indented;
                xmlWriter.Indentation = 2;
                xmlWriter.IndentChar = ' ';

                xmlDoc.AppendChild(rootElement);

                foreach (string civId in this.Keys)
                {
                    XmlElement homeSystemElement = xmlDoc.CreateElement("HomeSystem");
                    homeSystemElement.SetAttribute("Civilization", civId);
                    this[civId].AppendXml(homeSystemElement);
                    rootElement.AppendChild(homeSystemElement);
                }

                xmlDoc.WriteTo(xmlWriter);
            }
        }
    }

    [Serializable]
    public sealed class StarSystemDescriptor : INotifyPropertyChanged
    {
        #region Fields
        private BindingList<PlanetDescriptor> _planets;
        private string _systemName;
        private string _inhabitants;
        private StarType? _starType;
        private SystemBonus _bonuses;
        #endregion

        #region Constructors
        public StarSystemDescriptor()
        {
            this._planets = new BindingList<PlanetDescriptor>();
        }
        #endregion

        public bool HasBonus(SystemBonus bonus)
        {
            return ((bonus & _bonuses) == bonus);
        }

        public void AddBonus(SystemBonus bonus)
        {
            _bonuses |= bonus;
        }

        public void RemoveBonus(SystemBonus bonus)
        {
            _bonuses &= ~bonus;
        }

        #region Properties
        public SystemBonus Bonuses
        {
            get { return _bonuses; }
            set
            {
                _bonuses = value;
                OnPropertyChanged("Bonuses");
                OnPropertyChanged("HasBonuses");
            }
        }

        [DependsOn("Bonuses")]
        public bool HasBonuses
        {
            get { return (this._bonuses != 0); }
        }

        public string Inhabitants
        {
            get { return _inhabitants; }
            set
            {
                _inhabitants = value;
                OnPropertyChanged("Inhabitants");
                OnPropertyChanged("IsInhabitantsDefined");
            }
        }

        public BindingList<PlanetDescriptor> Planets
        {
            get { return this._planets; }
            internal set
            {
                if (value != this._planets)
                {
                    if (value == null)
                        throw new ArgumentNullException();
                    this._planets = value;
                }
            }
        }

        public string Name
        {
            get { return this._systemName ?? String.Empty; }
            set
            {
                if (value != this._systemName)
                {
                    this._systemName = (value != null) ? value.Trim() : null;
                    OnPropertyChanged("Name");
                    OnPropertyChanged("IsNameDefined");
                }
            }
        }

        public StarType? StarType
        {
            get { return _starType; }
            set {
                if (value != this._starType)
                {
                    this._starType = value;
                    OnPropertyChanged("StarType");
                    OnPropertyChanged("IsStarTypeDefined");
                }
            }
        }

        [DependsOn("Inhabitants")]
        public bool IsInhabitantsDefined
        {
            get { return (this._inhabitants != null); }
        }

        [DependsOn("StarType")]
        public bool IsStarTypeDefined
        {
            get { return (this._starType != null); }
        }

        [DependsOn("Name")]
        public bool IsNameDefined
        {
            get { return (this._systemName != null); }
        }
        #endregion

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Methods
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

        public StarSystemDescriptor(XmlElement xmlNode) : this()
        {
            List<SystemBonus> bonuses = new List<SystemBonus>();

            if (xmlNode.HasAttribute("Name"))
                this.Name = xmlNode.GetAttribute("Name").Trim();
            if (xmlNode.HasAttribute("StarType") && Enum.IsDefined(typeof(StarType), xmlNode.GetAttribute("StarType").Trim()))
                this.StarType = (StarType)Enum.Parse(typeof(StarType), xmlNode.GetAttribute("StarType").Trim());
            if (xmlNode["Inhabitants"] != null)
                this.Inhabitants = xmlNode["Inhabitants"].InnerText.Trim().ToUpperInvariant();

            foreach (XmlElement bonusElement in xmlNode.GetElementsByTagName("Bonus"))
            {
                if (Enum.IsDefined(typeof(SystemBonus), bonusElement.GetAttribute("Type").Trim()))
                {
                    bonuses.Add((SystemBonus)Enum.Parse(typeof(SystemBonus), bonusElement.GetAttribute("Type").Trim()));
                }
            }

            foreach (SystemBonus bonus in bonuses)
            {
                AddBonus(bonus);
            }

            if (xmlNode["Planets"] != null)
            {
                foreach (XmlElement planetElement in xmlNode["Planets"].GetElementsByTagName("Planet"))
                {
                    this.Planets.Add(new PlanetDescriptor(planetElement));
                }
            }
        }

        public void AppendXml(XmlElement baseElement)
        {
            XmlElement systemElement = baseElement.OwnerDocument.CreateElement("StarSystem");

            if (this.IsNameDefined)
                systemElement.SetAttribute("Name", this.Name);
            if (this.IsStarTypeDefined)
                systemElement.SetAttribute("StarType", this.StarType.Value.ToString());
            
            if (this.IsInhabitantsDefined)
            {
                XmlElement inhabitantsElement = systemElement.OwnerDocument.CreateElement("Inhabitants");
                inhabitantsElement.InnerText = this.Inhabitants;
                systemElement.AppendChild(inhabitantsElement);
            }

            if (this.HasBonuses)
            {
                XmlElement bonusesElement = systemElement.OwnerDocument.CreateElement("Bonuses");
                foreach (SystemBonus bonus in Enum.GetValues(typeof(SystemBonus)))
                {
                    if ((bonus != SystemBonus.NoBonus) && HasBonus(bonus))
                    {
                        XmlElement bonusElement = systemElement.OwnerDocument.CreateElement("Bonus");
                        bonusElement.SetAttribute("Type", bonus.ToString());
                        bonusesElement.AppendChild(bonusElement);
                    }
                }
                systemElement.AppendChild(bonusesElement);
            }

            if (this.Planets.Count > 0)
            {
                XmlElement planetsElement = systemElement.OwnerDocument.CreateElement("Planets");
                foreach (PlanetDescriptor planet in this.Planets)
                {
                    XmlElement planetElement = systemElement.OwnerDocument.CreateElement("Planet");
                    planet.AppendXml(planetElement);
                    planetsElement.AppendChild(planetElement);
                }
                systemElement.AppendChild(planetsElement);
            }

            baseElement.AppendChild(systemElement);
        }
    }

    [Serializable]
    public sealed class PlanetDescriptor : INotifyPropertyChanged
    {
        #region Fields
        private int _maxNumberOfPlanets;
        private int _minNumberOfPlanets;
        private string _planetName;
        private PlanetSize? _planetSize;
        private PlanetType? _planetType;
        private PlanetBonus _bonuses;
        #endregion

        public bool HasBonus(PlanetBonus bonus)
        {
            return ((bonus & _bonuses) == bonus);
        }

        public void AddBonus(PlanetBonus bonus)
        {
            _bonuses |= bonus;
        }

        public void RemoveBonus(PlanetBonus bonus)
        {
            _bonuses &= ~bonus;
        }

        #region Properties
        public PlanetBonus Bonuses
        {
            get { return _bonuses; }
            set
            {
                _bonuses = value;
                OnPropertyChanged("Bonuses");
                OnPropertyChanged("HasBonuses");
            }
        }

        [DependsOn("Bonuses")]
        public bool HasBonuses
        {
            get { return (this._bonuses != 0); }
        }

        [DependsOn("IsSinglePlanet")]
        [DependsOn("Name")]
        public bool IsNameDefined
        {
            get { return (_planetName != null); }
        }

        [DependsOn("IsSinglePlanet")]
        [DependsOn("Size")]
        public bool IsSizeDefined
        {
            get { return (IsSinglePlanet && (_planetSize != null)); }
        }

        [DependsOn("IsSinglePlanet")]
        [DependsOn("Type")]
        public bool IsTypeDefined
        {
            get { return (IsSinglePlanet && (_planetType != null)); }
        }

        [DependsOn("IsSinglePlanet")]
        public string Name
        {
            get { return (IsSinglePlanet ? (_planetName ?? String.Empty) : String.Empty); }
            set
            {
                if (value != _planetName)
                {
                    _planetName = (value != null) ? value.Trim() : null;
                    OnPropertyChanged("Name");
                    OnPropertyChanged("IsNameDefined");
                }
            }
        }

        [DependsOn("MaxNumberOfPlanets")]
        public bool IsSinglePlanet
        {
            get { return (this._maxNumberOfPlanets == 1); }
        }

        public int MinNumberOfPlanets
        {
            get { return this._minNumberOfPlanets; }
            set
            {
                if (value != this._minNumberOfPlanets)
                {
                    if (value < 0)
                        throw new ArgumentOutOfRangeException("value", "value must be non-negative");
                    this._minNumberOfPlanets = value;
                    OnPropertyChanged("MinNumberOfPlanets");
                }
            }
        }

        public int MaxNumberOfPlanets
        {
            get { return this._maxNumberOfPlanets; }
            set
            {
                if (value != this._maxNumberOfPlanets)
                {
                    if (value < 1)
                        throw new ArgumentOutOfRangeException("value", "value must be greater than zero");
                    if (value > StarSystem.MaxPlanetsPerSystem)
                        throw new ArgumentOutOfRangeException("value", "value must be less than MaxPlanetsPerSystem");
                    this._maxNumberOfPlanets = value;
                    OnPropertyChanged("MaxNumberOfPlanets");
                    OnIsSinglePlanetChanged();
                }
            }
        }

        [DependsOn("IsSinglePlanet")]
        public PlanetType? Type
        {
            get { return this._planetType; }
            set
            {
                if (value != this._planetType)
                {
                    this._planetType = value;
                    OnPropertyChanged("Type");
                    OnPropertyChanged("IsTypeDefined");
                    if (this._planetType == PlanetType.Asteroids)
                        this.Size = PlanetSize.Asteroids;
                    else if (this._planetType == PlanetType.GasGiant)
                        this.Size = PlanetSize.GasGiant;
                }
            }
        }

        [DependsOn("IsSinglePlanet")]
        public PlanetSize? Size
        {
            get { return this._planetSize; }
            set
            {
                if (value != this._planetSize)
                {
                    this._planetSize = value;
                    OnPropertyChanged("Size");
                    OnPropertyChanged("IsSizeDefined");
                    if (this._planetSize == PlanetSize.Asteroids)
                        this.Type = PlanetType.Asteroids;
                    else if (this._planetSize == PlanetSize.GasGiant)
                        this.Type = PlanetType.GasGiant;
                }
            }
        }
        #endregion

        #region INotifyPropertyChanged Members
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Methods
        private void OnIsSinglePlanetChanged()
        {
            OnPropertyChanged("IsSinglePlanet");
            OnPropertyChanged("IsNameDefined");
            OnPropertyChanged("IsSizeDefined");
            OnPropertyChanged("IsTypeDefined");
        }

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public PlanetDescriptor()
        {
            _minNumberOfPlanets = 1;
            _maxNumberOfPlanets = 1;
        }

        public PlanetDescriptor(XmlElement xmlNode) : this()
        {
            int tempInteger;
            List<PlanetBonus> bonuses = new List<PlanetBonus>();

            if (xmlNode.HasAttribute("Name"))
                this.Name = xmlNode.GetAttribute("Name").Trim();
            if (xmlNode.HasAttribute("Size") && Enum.IsDefined(typeof(PlanetSize), xmlNode.GetAttribute("Size").Trim()))
                this.Size = (PlanetSize)Enum.Parse(typeof(PlanetSize), xmlNode.GetAttribute("Size").Trim());
            if (xmlNode.HasAttribute("Type") && Enum.IsDefined(typeof(PlanetType), xmlNode.GetAttribute("Type").Trim()))
                this.Type = (PlanetType)Enum.Parse(typeof(PlanetType), xmlNode.GetAttribute("Type").Trim());
            if ((xmlNode.HasAttribute("MaxNumberOfPlanets")) && int.TryParse(xmlNode.GetAttribute("MaxNumberOfPlanets").Trim(), out tempInteger))
                this.MaxNumberOfPlanets = tempInteger;
            if ((xmlNode.HasAttribute("MinNumberOfPlanets")) && int.TryParse(xmlNode.GetAttribute("MinNumberOfPlanets").Trim(), out tempInteger))
                this.MinNumberOfPlanets = tempInteger;

            foreach (XmlElement bonusElement in xmlNode.GetElementsByTagName("Bonus"))
            {
                if (Enum.IsDefined(typeof(PlanetBonus), bonusElement.GetAttribute("Type").Trim()))
                {
                    bonuses.Add((PlanetBonus)Enum.Parse(typeof(PlanetBonus), bonusElement.GetAttribute("Type").Trim()));
                }
            }

            foreach (PlanetBonus bonus in bonuses)
            {
                AddBonus(bonus);
            }

            if (!this.IsSizeDefined && this.IsTypeDefined)
            {
                if (this.Type.Value == PlanetType.Asteroids)
                    this.Size = PlanetSize.Asteroids;
                else if (this.Type.Value == PlanetType.GasGiant)
                    this.Size = PlanetSize.GasGiant;
            }
        }

        public void AppendXml(XmlElement baseElement)
        {
            if (this.IsSinglePlanet)
            {
                if (this.IsNameDefined)
                    baseElement.SetAttribute("Name", this.Name);
                if (this.IsTypeDefined)
                    baseElement.SetAttribute("Type", this.Type.ToString());
                if (this.IsSizeDefined)
                    baseElement.SetAttribute("Size", this.Size.ToString());
                if (this.HasBonuses)
                {
                    XmlElement bonusElement;
                    foreach (PlanetBonus bonus in Enum.GetValues(typeof(PlanetBonus)))
                    {
                        if ((bonus != PlanetBonus.NoBonus) && HasBonus(bonus))
                        {
                            bonusElement = baseElement.OwnerDocument.CreateElement("Bonus");
                            bonusElement.SetAttribute("Type", bonus.ToString());
                            baseElement.AppendChild(bonusElement);
                        }
                    }
                }
            }
            else
            {
                baseElement.SetAttribute("MinNumberOfPlanets", this.MinNumberOfPlanets.ToString());
                baseElement.SetAttribute("MaxNumberOfPlanets", this.MaxNumberOfPlanets.ToString());
            }
        }
        #endregion
    }
}